<?php

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Web services utility functions and classes
 *
 * @package   cm_webservice
 * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */


// Uses the cut down Zend files from moodle
require_once 'Zend/XmlRpc/Server.php';

function get_service_info() {
	global $CFG;

	$username = 'admin';
	if (isset($CFG->local_cm_enrol_rpc_enabled) && empty($CFG->local_cm_enrol_rpc_enabled) == false) {
		$username = $CFG->local_cm_enrol_rpc_username;
	}

	$tokens = array();
	if (isset($CFG->local_cm_enrol_rpc_token) && empty($CFG->local_cm_enrol_rpc_token) == false) {
		$tokens[] = $CFG->local_cm_enrol_rpc_token;
	} else {
		throw new cm_webservice_access_exception('Course Merchant RPC token is not configured!');
	}

	$cm_service_info = new stdClass();
	$cm_service_info->username = $username;
	$cm_service_info->tokens = $tokens;
	$cm_service_info->iprestriction = null; // e.g. '127.0.0.1/24'
	return $cm_service_info;
}

/**
 * Exception indicating access control problem in web service call
 */
class cm_webservice_access_exception extends moodle_exception {
	/**
	* Constructor
	*/
	function __construct($debuginfo) {
		parent::__construct('accessexception', 'cm_webservice', '', null, $debuginfo);
	}
}



/**
 * XML-RPC service server implementation.
 */
class cm_webservice_xmlrpc_server {

	/** @property string name of the zend server class : Zend_XmlRpc_Server, Zend_Soap_Server, Zend_Soap_AutoDiscover, ...*/
	protected $zend_class;

	/** @property object Zend server instance */
	protected $zend_server;

	/** @property string $service_namespace web service namespace */
	protected $service_namespace;

	/** @property string $service_class web service class */
	protected $service_class;

	/** @property int $userid the local user */
	protected $userid = null;

	/** @property object $auth provide authentication methods */
	protected $auth;

	/**
	* Contructor
	* @param integer $authmethod authentication method - one of WEBSERVICE_AUTHMETHOD_*
	*/
	public function __construct($auth) {
		$this->auth = $auth;
		$this->zend_class = 'CM_Zend_XmlRpc_Server';
	}

	/**
	* This method has to be called before every operation
	* that takes a longer time to finish!
	*
	* @param int $seconds max expected time the next operation needs
	* @return void
	*/
	public function set_timeout($seconds=360) {
		$seconds = ($seconds < 300) ? 300 : $seconds;
		set_time_limit($seconds);
	}

	/**
	* Process request from client.
	* @param bool $simple use simple authentication
	* @return void
	*/
	public function run() {
		error_reporting(E_ALL | E_NOTICE);
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		// we will probably need a lot of memory in some functions
		raise_memory_limit(MEMORY_EXTRA);

		// set some longer timeout, this script is not sending any output,
		// this means we need to manually extend the timeout operations
		// that need longer time to finish
		$this->set_timeout();

		// now create the instance of zend server
		$this->init_zend_server();

		// set up exception handler first, we want to sent them back in correct format that
		// the other system understands
		// we do not need to call the original default handler because this ws handler does everything
		set_exception_handler(array($this, 'exception_handler'));

		// this sets up $USER and $SESSION and context restrictions
		$this->userid = $this->auth->authenticate();

		// make a list of all functions user is allowed to excecute
		$this->init_service_class();

		// require_once 'Zend/XmlRpc/Server/Cache.php';
		// require_once 'Zend/XmlRpc/Server/Fault.php';
		// $cacheFile = dirname(__FILE__) . '/xmlrpc.cache';
		// tell server what functions are available
		// if (!Zend_XmlRpc_Server_Cache::get($cacheFile, $this->zend_server)) {
		$this->zend_server->setClass($this->service_class, $this->service_namespace);
		Zend_XmlRpc_Server_Fault::attachFaultException('CM_RPC_EXCEPTION');
		Zend_XmlRpc_Server_Fault::attachFaultException('moodle_exception');
		// Save cache
		//     Zend_XmlRpc_Server_Cache::save($cacheFile, $this->zend_server);
		// }

		//log the web service request
		add_to_log(SITEID, 'cm_rpc', '', '' , $this->zend_class." ".getremoteaddr() , 0, $this->userid);

		// execute and return response, this sends some headers too
		$response = $this->zend_server->handle();

		//finally send the result
		$this->send_headers();
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		// error_log(print_r($response, true));
		echo $response;
		die;
	}

	/**
	* Load virtual class needed for Zend api
	* @return void
	*/
	protected function init_service_class() {
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		global $CFG;

		// require_once(dirname(__FILE__) .'/../externallib.php');
		require_once(dirname(__FILE__) .'/../lib/coursemerchant_rpc_v4.php');
		$this->service_class = 'coursemerchant_rpc_v4';
		// $this->service_class = 'local_coursemerchant_external';
		$this->service_namespace = 'coursemerchant_rpc_v4';
	}


	/**
	* Set up zend service class
	* @return void
	*/
	protected function init_zend_server() {
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		$this->zend_server = new $this->zend_class();
	}

	/**
	* Internal implementation - sending of page headers.
	* @return void
	*/
	protected function send_headers() {
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
		header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
		header('Pragma: no-cache');
		header('Accept-Ranges: none');
	}

	/**
	* Specialised exception handler, we can not use the standard one because
	* it can not just print html to output.
	*
	* @param exception $ex
	* @return void does not return
	*/
	public function exception_handler($ex) {
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		// error_log(print_r($ex, true));
		// detect active db transactions, rollback and log as error
		abort_all_db_transactions();

		// now let the plugin send the exception to client
		$this->send_error($ex);

		// not much else we can do now, add some logging later
		exit(1);
	}

	/**
	* Send the error information to the WS client
	* formatted as XML document.
	* @param exception $ex
	* @return void
	*/
	protected function send_error($ex=null) {
		// error_log(__FILE__ .':'.__METHOD__.':'.__LINE__);
		$this->send_headers();
		echo $this->zend_server->fault($ex);
	}

}

// Subclass Zend XML RPC server so we can disable parameter signature validation for now.
class CM_Zend_XmlRpc_Server extends Zend_XmlRpc_Server {
	/**
	* Handle an xmlrpc call (actual work)
	*
	* @param Zend_XmlRpc_Request $request
	* @return Zend_XmlRpc_Response
	* @throws Zend_XmlRpcServer_Exception|Exception
	* Zend_XmlRpcServer_Exceptions are thrown for internal errors; otherwise,
	* any other exception may be thrown by the callback
	*/
	protected function _handle(Zend_XmlRpc_Request $request) {
		$method = $request->getMethod();

		// Check for valid method
		if (!$this->_table->hasMethod($method)) {
			require_once 'Zend/XmlRpc/Server/Exception.php';
			throw new Zend_XmlRpc_Server_Exception('Method "' . $method . '" does not exist', 620);
		}

		$info     = $this->_table->getMethod($method);
		$params   = $request->getParams();
		$argv     = $info->getInvokeArguments();
		if (0 < count($argv) and $this->sendArgumentsToAllMethods()) {
			$params = array_merge($params, $argv);
		}

		// Check calling parameters against signatures
		$matched    = false;
		$sigCalled  = $request->getTypes();

		$sigLength  = count($sigCalled);
		$paramsLen  = count($params);
		if ($sigLength < $paramsLen) {
			for ($i = $sigLength; $i < $paramsLen; ++$i) {
				$xmlRpcValue = Zend_XmlRpc_Value::getXmlRpcValue($params[$i]);
				$sigCalled[] = $xmlRpcValue->getType();
			}
		}

		// FIXME: Disable function paramater signature verification
		// Let implicit casting work for now for bools vs. int, arrays vs. structs, etc. until we
		// can clamp down the client side more rigidly.

		// $signatures = $info->getPrototypes();
		// foreach ($signatures as $signature) {
		// 	$sigParams = $signature->getParameters();
		// 	if ($sigCalled === $sigParams) {
		// 		$matched = true;
		// 		break;
		// 	}
		// }
		// if (!$matched) {
		// 	require_once 'Zend/XmlRpc/Server/Exception.php';
		// 	throw new Zend_XmlRpc_Server_Exception('Calling parameters do not match signature', 623);
		// }

		$return        = $this->_dispatch($info, $params);
		$responseClass = $this->getResponseClass();
		return new $responseClass($return);
	}
}
